/*
 * $Id: isbnlookup.c 29 2010-12-04 13:19:29Z vext01 $
 *
 * Copyright (c) 2010, Edd Barrett <vext01@gmail.com>
 * 
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * Stuff to do:
 *   search all parameters
 *   different search modes
 *   eventually build as a shared library
 *   iterators to sheild library users from RB_*
 */

/* linux needs this for asprintf() */
#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <err.h>
#include <string.h>
#include <ctype.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/types.h>
#include <regex.h>
#include <stdarg.h>

#include <libxml/tree.h>
#include <libxml/parser.h>

#include "isbnlookup.h"

#define ISBN_URL_HEAD	"http://isbndb.com/api/books.xml?access_key="
#define ISBN_URL_TAIL	"&index1=isbn&results=details&value1="

RB_GENERATE(isbn_fields_t, isbn_field, entry, isbn_field_cmp)
RB_GENERATE(isbn_books_t, isbn_book, entry, isbn_book_cmp)

/* globals */
const xmlChar	*last_el, *last_id;
int		parser_in_bookdata = 0;
uint8_t		bflag, uflag, dflag;
char		api_key[9];
struct isbn_book	*last_book;

#define DPRINTF(...) \
	do { if (dflag) fprintf(stderr, __VA_ARGS__); } while (0)

void *
xmalloc(size_t sz)
{
	void		*ptr;

	ptr = malloc(sz);
	if (ptr == NULL)
		err(1, "cannot allocate memory");

	return ptr;
}

/* for red-black tree lookup via key */
int
isbn_field_cmp(struct isbn_field *e1, struct isbn_field *e2)
{
	return xmlStrcmp(e1->key, e2->key);
}

int
isbn_book_cmp(struct isbn_book *e1, struct isbn_book *e2)
{
	return xmlStrcmp(e1->id, e2->id);
}

int
xasprintf(char **ret, const char *fmt, ...)
{
	int		iret;
	va_list		argp;

	va_start(argp, fmt);

	iret = vasprintf(ret, fmt, argp);

	if (iret < 0)
		warn("%s: couldn't allocate\n", __func__);

	return iret;
}

void
isbn_add_field(struct isbn_book *bk, const xmlChar *key, const xmlChar *val)
{
	struct isbn_field	*field;
	const xmlChar		*c;
	xmlChar			*n_key, *cc;

	/* make the key sane */
	n_key = xmalloc(xmlStrlen(key));
	for (c = key, cc = n_key; *c != NULL_XCHAR; c ++) {
		if (*c == '_')
			continue;
		*cc = tolower(*c);
		cc ++;
	}
	*cc = NULL_XCHAR;

	DPRINTF("adding field: '%s' = '%s'\n", n_key, val);

	if (bk == NULL)
		warn("%s: adding field '%s' to NULL book!\n", __func__, (char *) key);

	field = xmalloc(sizeof(struct isbn_field));
	field->key = n_key;
	field->val = xmlStrdup(val);
	RB_INSERT(isbn_fields_t, &bk->fields, field);
}

int
isbn_try_add_attr(struct isbn_book *bk, xmlNode *n, char *attr, int required)
{
	xmlChar		*val;

	DPRINTF("%s: try to add field from attribute '%s'\n", __func__, attr);

	val = xmlGetProp(n, (xmlChar *) attr);
	if (val)
		isbn_add_field(bk, (xmlChar *)attr, val);
	else if (required == 1) {
		warn("required field '%s' is missing\n", attr);
		return EXIT_FAILURE;
	}
	xmlFree(val);

	return EXIT_SUCCESS;
}

/*
 * there are a whole bunch of attributes we need to fetch too
 */
void
isbn_parse_attributes(struct isbn_book *bk, xmlNode *n)
{
	/* XXX way of automagically iterating any fields that might be there */
	if (xmlStrcmp(n->name, XC("BookData")) == 0) {
		isbn_try_add_attr(bk, n, "book_id", 1);
		isbn_try_add_attr(bk, n, "isbn", 0);
		isbn_try_add_attr(bk, n, "isbn13", 0);
	} else if (xmlStrcmp(n->name, XC("Details")) == 0) {
		isbn_try_add_attr(bk, n, "change_time", 0);
		isbn_try_add_attr(bk, n, "price_time", 0);
		isbn_try_add_attr(bk, n, "edition_info", 0);
		isbn_try_add_attr(bk, n, "language", 0);
		isbn_try_add_attr(bk, n, "physical_description_text", 0);
		isbn_try_add_attr(bk, n, "lcc_number", 0);
		isbn_try_add_attr(bk, n, "dewey_decimal_normalized", 0);
		isbn_try_add_attr(bk, n, "dewey_decimal", 0);
	} else if (xmlStrcmp(n->name, XC("PublisherText")) == 0)
		isbn_try_add_attr(bk, n, "publisher_id", 0);
}

/*
 * XXX this will build the url itself once I have a 
 * struct defined for querying
 */
struct isbn_query *
isbn_query(char *url)
{
	xmlDoc			*doc;
	xmlNode			*root;
	struct isbn_query	*q;

	doc = xmlReadFile(url, NULL, 0);
	if (doc == NULL) {
		fprintf(stderr, "can not parse url\n");
		return NULL;
	}

	root = xmlDocGetRootElement(doc);
	q = isbn_parse_response(NULL, NULL, root);

	/* there may be a book hanging after, we need to add it */
	if ((last_book) && (!RB_EMPTY(&last_book->fields))) {
		DPRINTF("%s: added hanging book\n", __func__);
		isbn_add_book(q, last_book);
	} else
		free(last_book);

	if (doc)
		xmlFreeDoc(doc);

	return q;
}

void
isbn_add_book(struct isbn_query *q, struct isbn_book *bk)
{
	struct isbn_field	*id;

	id = isbn_get_field(bk, XC("bookid"));
	bk->id = id->val;

	RB_INSERT(isbn_books_t, &q->books, bk);
	q->num_books ++;
}

struct isbn_query
*isbn_parse_response(struct isbn_query *q, struct isbn_book *bk, xmlNode *n)
{
	xmlNode			*this_node;

	/* NULL query means this is the top level call */
	if (q == NULL)
		q = isbn_new_query();

	for (this_node = n; this_node; this_node = this_node->next) {
		/* if an element, traverse deeper */
		if (this_node->type == XML_ELEMENT_NODE) {

			DPRINTF("%s: element_node '%s'\n",
			    __func__, this_node->name);

			/*
			 * hit next book now, store last one and
			 * allocate next.
			 */
			if (xmlStrcmp(this_node->name, XC("BookData")) == 0) {
				if (bk != NULL) {
					isbn_add_book(q, bk);
				}
				bk = isbn_new_book();
				last_book = bk;
			}

			/* parse attributes, if any */
			isbn_parse_attributes(bk, this_node);

			last_el = this_node->name;
			isbn_parse_response(q, bk, this_node->children);
		/* if text */
		} else if ((this_node->type == XML_TEXT_NODE) &&
		    (this_node->parent->name == last_el)) {

			DPRINTF("%s: text_node '%s'\n",
			    __func__, this_node->content);

			/* make sure we are in the interesting part of XML */
			if (xmlStrcmp(this_node->parent->name,
			    XC("BookData")) == 0) {
				/* from here on, interesting */
				DPRINTF("%s: found BookData element\n", __func__);
				parser_in_bookdata = 1;

			} else if ((last_el != NULL) && (parser_in_bookdata)) {
				isbn_add_field(bk, last_el, this_node->content);
			}
		}

	}

	return q;
}

struct isbn_field *
isbn_get_field(struct isbn_book *bk, xmlChar *f)
{
	struct isbn_field	find;

	find.key = f;
	return RB_FIND(isbn_fields_t, &bk->fields, &find);
}

#define ISBN_YEAR_REGEX_MAX	4
#define ISBN_YEAR_REGEX_ERR_MAX	64
xmlChar *
isbn_find_year(xmlChar *str)
{
	regex_t		reg;
	regmatch_t	matches[ISBN_YEAR_REGEX_MAX];
	int		ok, i;
	char		errstr[ISBN_YEAR_REGEX_ERR_MAX];
	xmlChar		*year;

	str = str; /* XXX */

	/* 1XXX or 2XXX */ 
	if (regcomp(&reg, "[12][0-9]{3}", REG_EXTENDED) != 0) {
		warn("%s: cannot compile regex\n", __func__);
		return NULL;
	}

	ok = regexec(&reg, (char *) str, ISBN_YEAR_REGEX_MAX, matches, 0);
	if (ok == REG_NOMATCH) {
		/* fine, no match, exit */
		regfree(&reg);
		return NULL;
	} else if (ok != 0) {
		regerror(ok, &reg, errstr, ISBN_YEAR_REGEX_ERR_MAX);
		warn("%s: %s", __func__, errstr);
	}

	/* got a match */
	DPRINTF("%s: year match offsets: %d %d\n", __func__, 
	    (int) matches[0].rm_so, (int) matches[0].rm_eo);

	year = xmalloc(5);
	for (i = 0; i < 4; i ++)
		year[i] = str[matches[0].rm_so + i];

	year[4] = 0;

	DPRINTF("%s: year found: %s\n", __func__, year);

	regfree(&reg);
	return year;
}

xmlChar *
isbn_bibfilter_year(xmlChar *in)
{
	xmlChar		*year;
	uint8_t		i;

	in = in + (xmlStrlen(in)) - 6;

	year = xmalloc(5);
	for (in ++, i = 0; i < 4; i ++) {
		if (isdigit(in[i]) == 0) {
			warn("malformed publishing year?");
			xmlFree(year);
			return NULL;
		}
		year[i] = in[i];
	}
	year[4] = 0;

	return year;
}

/* backup plan - look in editioninfo for a year */
xmlChar *
isbn_bibfilter_year2(xmlChar *in)
{
	isbn_find_year(in);
	return NULL;
}

/* XXX this may not re robust enough */
xmlChar *
isbn_bibfilter_publisher(xmlChar *in)
{
	xmlChar		*pub;
	xmlChar		*c;
	uint8_t		i, foundyear = 0;

	c = in + (xmlStrlen(in)) - 5;

	for (i = 0; i < 4; i ++) {
		if (isdigit(c[i]) == 0)
			goto doit;
	}
	foundyear = 1;

doit:
	pub = xmlStrdup(in);

	if (foundyear)
		pub[xmlStrlen(pub) - 8] = 0;

	return pub;

}

struct bibtex_mapping {
	xmlChar			*from;
	xmlChar			*to;
	xmlChar			*(*filter)(xmlChar *);
};

/* FILTERS SHOULD ALWAYS ALLOCATE IF THEY DONT RETURN NULL */
struct bibtex_mapping bibtex_mappings[] = {
	{ XC("authorstext"),	XC("author"),	NULL },
	{ XC("title"),		XC("title"),	NULL },
	{ XC("publishertext"),	XC("publisher"),isbn_bibfilter_publisher },
	{ XC("editioninfo"),	XC("edition"),	NULL },
	{ XC("publishertext"),	XC("year"),	isbn_find_year},
	{ XC("editioninfo"),	XC("year"),	isbn_find_year},
	{ NULL_XCHAR,		NULL_XCHAR,	NULL }
};

xmlChar *
isbn_to_bibtex(struct isbn_book *bk)
{
	xmlChar 		*ret = NULL, *old_ret, *filtered = NULL;
	struct bibtex_mapping	*map;
	struct isbn_field	*field;

	field = isbn_get_field(bk, XC("bookid"));
	xasprintf((char **) &ret, "@book{%s,\n", field->val);
	for (map = bibtex_mappings; map->from != NULL; map ++) {
		DPRINTF("%s: mapping bibtex field '%s'\n", __func__, map->to);
		field = isbn_get_field(bk, map->from);	

		if (!field)
			continue;

		/* if there is a filter, we apply it */
		filtered = NULL;
		if (map->filter)
			filtered = map->filter(field->val);
		else
			filtered = xmlStrdup(field->val);

		if (filtered == NULL)
			continue;

		old_ret = ret;
		xasprintf((char **) &ret, "%s\t%s = \"%s\",\n", 
		    ret, map->to, filtered);
		xmlFree(old_ret);
		xmlFree(filtered);
	}

	old_ret = ret;
	xasprintf((char **) &ret, "%s}\n", ret);
	xmlFree(old_ret);
	
	return ret;
}

xmlChar *
isbn_to_text(struct isbn_book *bk)
{
	xmlChar			*ret = XC("");
	struct isbn_field	*field;

	RB_FOREACH(field, isbn_fields_t, &bk->fields)
		xasprintf((char **) &ret, "%s%s: '%s'\n",
		    (char *) ret, (char *) field->key, (char *) field->val);

	return ret;
}

void
isbn_free_field(struct isbn_field *f)
{
	xmlFree(f->key);
	xmlFree(f->val);
	free(f);
}

void
usage()
{
	printf("Usage: isbnlookup [args] <isbncode>\n");
	printf("	-b	BibTeX output.\n");
	printf("	-d	Show debugging messages.\n");
	printf("	-h	Show help.\n");
	printf("	-u	Print isbndb.com URL.\n");
	printf("	-v	Show version.\n");
}

int
isbn_read_config()
{
	char		*home, *path;
	FILE		*handle;
       
	home = getenv("HOME");

	if (!home) {
		perror(__func__);
		return EXIT_FAILURE;
	}

	xasprintf(&path, "%s/%s", home, ISBN_CONFIG_FILE);	
	DPRINTF("%s: reading configuration from %s\n", __func__, path);

	handle = fopen(path, "r");
	free(path);

	if (handle == NULL) {
		perror(__func__);
		return EXIT_FAILURE;
	}

	if (fgets(api_key, 9, handle) == NULL) {
		perror(__func__);
		return EXIT_FAILURE;
	}

	DPRINTF("%s: found API key '%s'\n", __func__, api_key);

	fclose(handle);

	return EXIT_SUCCESS;
}

struct isbn_book *
isbn_new_book()
{
	struct isbn_book	*bk;

	DPRINTF("%s: allocating new book\n", __func__);

	bk = xmalloc(sizeof(struct isbn_book));
	RB_INIT(&bk->fields);

	return bk;
}

struct isbn_query *
isbn_new_query()
{
	struct isbn_query	*q;

	q = xmalloc(sizeof(struct isbn_query));
	RB_INIT(&q->books);
	q->num_books = 0;

	return q;
}

int
main(int argc, char **argv)
{
	char			*url = NULL;
	int			opt, exit_status = EXIT_SUCCESS, n = 0;
	xmlChar			*output = NULL;
	struct isbn_query	*q = NULL;
	struct isbn_book	*bk = NULL;

	DPRINTF("isbnlookup initialising...\n");

	LIBXML_TEST_VERSION

	while ((opt = getopt(argc, argv, "bdhuv")) != -1) {
		switch (opt) {
		case 'b':	/* bibtex output */
			bflag = 1;
			break;
		case 'd':	/* debug messages */
			dflag = 1;
			break;
		case 'h':	/* help */
			usage();
			goto clean;
			break;
		case 'u':	/* print URL */
			uflag = 1;
			break;
		case 'v':
			printf("This is ISBNLookup Version: "
			    ISBNLOOKUP_VERSION " :: ");
			printf("(c) Edd Barrett 2010\nThis code is released "
			    "under the terms of the ISC license\n");
			goto clean;
			break;
		default:
			usage();
			exit_status = EXIT_FAILURE;
			goto clean;
		}
	}
	argc -= optind;
	argv += optind;

	/* should be at most one thing left, the isbn code itself */
	if (argc != 1) {
		usage();
		exit_status = EXIT_FAILURE;
		goto clean;
	}

	if (isbn_read_config() == EXIT_FAILURE) {
		fprintf(stderr, "Something was wrong with your config file.\n");
		fprintf(stderr, "The API key is an 8 long code issued from: ");
		fprintf(stderr, "http://isbndb.com/\n");
		fprintf(stderr, "Obtain a key and place in '~/"
		    ISBN_CONFIG_FILE "'.\n");
	}

	xasprintf(&url, "%s%s%s%s", ISBN_URL_HEAD, api_key, ISBN_URL_TAIL, argv[0]);

	if (uflag) {
		printf("%s\n", url);
		goto clean;
	}

	DPRINTF("%s: url = '%s'\n", __func__, url);

	q = isbn_query(url);
	if (q == NULL)
		goto clean;

	if (q->num_books == 0) {
		fprintf(stderr, "No match\n");
		exit_status = EXIT_FAILURE;
		goto clean;
	} else {
		printf("Query contains %d books.\n\n", q->num_books);

		/* for now, we just print what we found */
		RB_FOREACH(bk, isbn_books_t, &q->books) {
			if (bflag) {
				printf("%% Book #%d:\n", n);
				output = isbn_to_bibtex(bk);
			} else {
				printf("Book #%d:\n", n);
				output = isbn_to_text(bk);
			}

			printf("%s\n", (char *) output);
			xmlFree(output);
			n ++;
		}
	}

clean:
	/* scrub, wash, rinse */

	if (q)
		isbn_free_query(q);

	if (url)
		free(url);

	xmlCleanupParser();

	return exit_status;
}

void
isbn_free_book(struct isbn_book *bk)
{
	struct isbn_field	*n, *next;

	for (n = RB_MIN(isbn_fields_t, &bk->fields); n != NULL; n = next) {
		next = RB_NEXT(isbn_fields_t, &bk->fields, n);
		RB_REMOVE(isbn_fields_t, &bk->fields, n);
		isbn_free_field(n);
	}
}

void
isbn_free_query(struct isbn_query *q)
{
	struct isbn_book	*n, *next;

	for (n = RB_MIN(isbn_books_t, &q->books); n != NULL; n = next) {
		next = RB_NEXT(isbn_books_t, &q->books, n);
		RB_REMOVE(isbn_books_t, &q->books, n);
		isbn_free_book(n);
	}

	free(q);


}
